﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

namespace Yoozoo.HRP.Runtime
{
    public class PlanarReflection : MonoBehaviour
    {
        private Camera reflectionCamera = null;
        public Camera viewCamera = null;
        public RenderTexture reflectionTexture = null;
        public Material reflectionMaterial = null;
        public int renderTextureSize = 512;
        private UniversalAdditionalCameraData _cameraData;
        private bool invertCulling;

        private void Start()
        {
            Initialize();
            
            invertCulling = GL.invertCulling;
            RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
            RenderPipelineManager.endCameraRendering += OnEndCameraRendering;
        }

        private void OnWillRenderObject()
        {
            //RenderReflection();
            // var reflectMatrix = CalculateReflectMatrix();
            // reflectionCamera.worldToCameraMatrix = viewCamera.worldToCameraMatrix * reflectMatrix;
            //
            // var normal = transform.up;
            // var dot = -Vector3.Dot(normal, transform.position);
            // var plane = new Vector4(normal.x, normal.y, normal.z, dot);
            // var clipMatrix = CalculateObliqueMatrix(plane, reflectionCamera);
            // reflectionCamera.projectionMatrix = clipMatrix;
        }

        private void RenderReflection()
        {
            // take main camera directions and position world space
            var mainCamTransform = viewCamera.transform;
            Vector3 cameraDirectionWorldSpace = mainCamTransform.forward;
            Vector3 cameraUpWorldSpace = mainCamTransform.up;
            Vector3 cameraPositionWorldSpace = mainCamTransform.position;

            //cameraPositionWorldSpace.y += verticalOffset;

            // transform direction and position by reflection plane
            var reflectionPlane = transform;
            Vector3 cameraDirectionPlaneSpace = reflectionPlane.InverseTransformDirection(cameraDirectionWorldSpace);
            Vector3 cameraUpPlaneSpace = reflectionPlane.InverseTransformDirection(cameraUpWorldSpace);
            Vector3 cameraPositionPlaneSpace = reflectionPlane.InverseTransformPoint(cameraPositionWorldSpace);

            // invert direction and position by reflection plane
            cameraDirectionPlaneSpace.y *= -1;
            cameraUpPlaneSpace.y *= -1;
            cameraPositionPlaneSpace.y *= -1;

            // transform direction and position from reflection plane local space to world space
            cameraDirectionWorldSpace = reflectionPlane.TransformDirection(cameraDirectionPlaneSpace);
            cameraUpWorldSpace = reflectionPlane.TransformDirection(cameraUpPlaneSpace);
            cameraPositionWorldSpace = reflectionPlane.TransformPoint(cameraPositionPlaneSpace);

            // apply direction and position to reflection camera
            var reflectionCamTransform = reflectionCamera.transform;
            reflectionCamTransform.position = cameraPositionWorldSpace;
            reflectionCamTransform.LookAt(cameraPositionWorldSpace + cameraDirectionWorldSpace, cameraUpWorldSpace);
        }

        private void Initialize()
        {
            if (reflectionCamera == null)
            {
                var go = new GameObject("Reflection " + name);
                reflectionCamera = go.AddComponent<Camera>();
                reflectionCamera.CopyFrom(viewCamera);
                //reflectionCamera.enabled = false;
                
                //reflectionCamera.hideFlags = HideFlags.HideAndDontSave;
            }
            
            UpdateCameraParams(viewCamera, reflectionCamera);
            
            if (reflectionTexture == null)
            {
                reflectionTexture = RenderTexture.GetTemporary(renderTextureSize, renderTextureSize, 24);
                reflectionTexture.name = "Reflection";
            }
            
            reflectionCamera.targetTexture = reflectionTexture;
            
            if (reflectionMaterial == null)
            {
                var attachedRenderer = GetComponent<Renderer>();
                reflectionMaterial = attachedRenderer.sharedMaterial;
            }

            if (reflectionMaterial)
                reflectionMaterial.SetTexture("_ReflectionTex", reflectionTexture);
        }
        
        private void OnBeginCameraRendering(ScriptableRenderContext scriptableRenderContext, Camera renderCamera)
        {
            if (viewCamera == null) return;
            if (reflectionMaterial == null) return;
            if (renderCamera != reflectionCamera) return;


            var reflectMatrix = CalculateReflectMatrix();
            reflectionCamera.worldToCameraMatrix = viewCamera.worldToCameraMatrix * reflectMatrix;

            var normal = transform.up;
            var dot = -Vector3.Dot(normal, transform.position);
            var plane = new Vector4(normal.x, normal.y, normal.z, dot);
            var clipMatrix = CalculateObliqueMatrix(plane, reflectionCamera);
            reflectionCamera.projectionMatrix = clipMatrix;

            GL.invertCulling = true;
            // UniversalRenderPipeline.RenderSingleCamera(scriptableRenderContext, reflectionCamera);
            // GL.invertCulling = oldInvertCulling;

        }

        private void OnEndCameraRendering(ScriptableRenderContext scriptableRenderContext, Camera renderCamera)
        {
            GL.invertCulling = invertCulling;
        }

        private void UpdateCameraParams(Camera srcCamera, Camera destCamera)
        {
            if (destCamera == null || srcCamera == null)
                return;

            destCamera.clearFlags = srcCamera.clearFlags;
            destCamera.backgroundColor = srcCamera.backgroundColor;
            destCamera.farClipPlane = srcCamera.farClipPlane;
            destCamera.nearClipPlane = srcCamera.nearClipPlane;
            destCamera.orthographic = srcCamera.orthographic;
            destCamera.fieldOfView = srcCamera.fieldOfView;
            destCamera.aspect = srcCamera.aspect;
            destCamera.orthographicSize = srcCamera.orthographicSize;
        }

        Matrix4x4 CalculateReflectMatrix()
        {
            var normal = this.transform.up;
            var d = -Vector3.Dot(normal, transform.position);
            var reflectM = new Matrix4x4();
            reflectM.m00 = 1 - 2 * normal.x * normal.x;
            reflectM.m01 = -2 * normal.x * normal.y;
            reflectM.m02 = -2 * normal.x * normal.z;
            reflectM.m03 = -2 * d * normal.x;

            reflectM.m10 = -2 * normal.x * normal.y;
            reflectM.m11 = 1 - 2 * normal.y * normal.y;
            reflectM.m12 = -2 * normal.y * normal.z;
            reflectM.m13 = -2 * d * normal.y;

            reflectM.m20 = -2 * normal.x * normal.z;
            reflectM.m21 = -2 * normal.y * normal.z;
            reflectM.m22 = 1 - 2 * normal.z * normal.z;
            reflectM.m23 = -2 * d * normal.z;

            reflectM.m30 = 0;
            reflectM.m31 = 0;
            reflectM.m32 = 0;
            reflectM.m33 = 1;
            return reflectM;
        }

        private Matrix4x4 CalculateObliqueMatrix(Vector4 plane, Camera camera)
        {
            var viewSpacePlane = camera.worldToCameraMatrix.inverse.transpose * plane;
            var projectionMatrix = camera.projectionMatrix;

            var clipSpaceFarPanelBoundPoint = new Vector4(Mathf.Sign(viewSpacePlane.x), Mathf.Sign(viewSpacePlane.y), 1, 1);
            var viewSpaceFarPanelBoundPoint = camera.projectionMatrix.inverse * clipSpaceFarPanelBoundPoint;

            var m4 = new Vector4(projectionMatrix.m30, projectionMatrix.m31, projectionMatrix.m32, projectionMatrix.m33);
            //u = 2 * (M4·E)/(E·P)，而M4·E == 1，化简得
            //var u = 2.0f * Vector4.Dot(m4, viewSpaceFarPanelBoundPoint) / Vector4.Dot(viewSpaceFarPanelBoundPoint, viewSpacePlane);
            var u = 2.0f / Vector4.Dot(viewSpaceFarPanelBoundPoint, viewSpacePlane);
            var newViewSpaceNearPlane = u * viewSpacePlane;

            //M3' = P - M4
            var m3 = newViewSpaceNearPlane - m4;

            projectionMatrix.m20 = m3.x;
            projectionMatrix.m21 = m3.y;
            projectionMatrix.m22 = m3.z;
            projectionMatrix.m23 = m3.w;

            return projectionMatrix;
        }

        private void OnDestroy()
        {
            RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
            RenderPipelineManager.endCameraRendering -= OnEndCameraRendering;
            if (reflectionTexture)
            {
                RenderTexture.ReleaseTemporary(reflectionTexture);
                reflectionTexture = null;
            }

            if (reflectionCamera)
            {
                Destroy(reflectionCamera.gameObject);
                reflectionCamera = null;
            }

            reflectionMaterial = null;
            viewCamera = null;
        }

        public void Toggle(bool value)
        {
            if (reflectionCamera)
                reflectionCamera.enabled = value;
        }
    } 
}

